<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="sskAF" id="sskAF"><span data-lake-id="ub4ec1f6f" id="ub4ec1f6f">典型回答</span></h1>
  <p data-lake-id="u79deba9c" id="u79deba9c"><br></p>
  <p data-lake-id="ub0509a82" id="ub0509a82"><span data-lake-id="u57cf9b25" id="u57cf9b25">滑动窗口限流是一种流量控制策略，用于控制在一定时间内允许执行的操作数量或请求频率。它的工作方式类似于一个滑动时间窗口，在窗口内允许的操作数量是固定的，窗口会随着时间的推移不断滑动。</span></p>
  <p data-lake-id="ufd28b4ef" id="ufd28b4ef"><span data-lake-id="u81b37bac" id="u81b37bac">​</span><br></p>
  <p data-lake-id="u1dab4a58" id="u1dab4a58"><br></p>
  <p data-lake-id="u550ec52d" id="u550ec52d"><span data-lake-id="u9e49f2f4" id="u9e49f2f4">滑动窗口限流的主要</span><strong><span data-lake-id="ua63796c6" id="ua63796c6">优点是可以在时间内平滑地控制流量</span></strong><span data-lake-id="u38719206" id="u38719206">，而不是简单地设置固定的请求数或速率。这使得系统可以更</span><strong><span data-lake-id="u3485098d" id="u3485098d">灵活地应对突发流量或峰值流量</span></strong><span data-lake-id="ua8d53ec2" id="ua8d53ec2">，而不会因为固定速率的限制而浪费资源或降低系统性能。</span></p>
  <p data-lake-id="u67a23500" id="u67a23500"><br></p>
  <p data-lake-id="ua3531a63" id="ua3531a63"><span data-lake-id="ud3e8ca38" id="ud3e8ca38">利用Redis，我们就可以实现一个简单的滑动窗口限流的功能。因为滑动窗口和时间有关，所以很容易能想到要基于时间进行统计。</span></p>
  <p data-lake-id="u05d4b840" id="u05d4b840"><span data-lake-id="u73078e78" id="u73078e78">​</span><br></p>
  <p data-lake-id="u5dbc4c25" id="u5dbc4c25"><strong><span data-lake-id="u43b44f9a" id="u43b44f9a">那么我们只需要在每一次有请求进来的时候，记录下请求的时间戳和请求的数据，然后在统计窗口内请求的数量时，只需要统计窗口内的被记录的数据量有多少条就行了。</span></strong></p>
  <p data-lake-id="u0d28bab7" id="u0d28bab7"><span data-lake-id="u471519dd" id="u471519dd">​</span><br></p>
  <p data-lake-id="u07bbfc29" id="u07bbfc29"><span data-lake-id="u2adde001" id="u2adde001">在Redis中，我们可以基于ZSET来实现这个功能。假如我们限定login接口一分钟只能调用100次：</span></p>
  <p data-lake-id="u67ecd602" id="u67ecd602"><span data-lake-id="u11082199" id="u11082199">​</span><br></p>
  <p data-lake-id="u597d4d54" id="u597d4d54"><span data-lake-id="u9f026809" id="u9f026809">那么，我们就可以把login接口这个需要做限流的资源名作为key在redis中进行存储，然后value我们现在ZSET这种数据结构，把他的score设置为当前请求的时间戳，member的话建议用请求的详情的hash进行存储（或者UUID、MD5什么的），避免在并发时，时间戳一致出现scode和memberv一样导致被zadd幂等的问题。</span></p>
  <p data-lake-id="u7bd7c757" id="u7bd7c757"><img src="https://cdn.nlark.com/yuque/0/2023/png/5378072/1697870905641-4dc17d40-7078-4c52-8e35-809d363812d4.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_22%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u6d635719" id="u6d635719"><br></p>
  <p data-lake-id="ub2b03236" id="ub2b03236"><br></p>
  <p data-lake-id="ub8c6ddd2" id="ub8c6ddd2"><span data-lake-id="ud8ca60ed" id="ud8ca60ed">所以，我们实现滑动窗口限流的主要思想是：</span><strong><span data-lake-id="ub7c7134e" id="ub7c7134e">只保留在特定时间窗口内的请求记录，而丢弃窗口之外的记录。</span></strong></p>
  <p data-lake-id="uc72da2be" id="uc72da2be"><span data-lake-id="u4d47ba12" id="u4d47ba12">​</span><br></p>
  <p data-lake-id="ueff505db" id="ueff505db"><span data-lake-id="ude932fad" id="ude932fad">主要步骤如下：<br><br></span></p>
  <ol list="u69b040cc">
   <li fid="u07213dc0" data-lake-id="uf74ce24d" id="uf74ce24d"><span data-lake-id="u693821cd" id="u693821cd">定义滑动窗口的时间范围，例如，窗口大小为60秒。</span></li>
   <li fid="u07213dc0" data-lake-id="ude18e9a3" id="ude18e9a3"><span data-lake-id="ub55fd441" id="ub55fd441">每次收到一个请求时，我们就定义出一个zset然后存储到redis中。</span></li>
   <li fid="u07213dc0" data-lake-id="u20d8fccd" id="u20d8fccd"><span data-lake-id="ue20ca673" id="ue20ca673">然后再通过ZREMRANGEBYSCORE命令来删除分值小于窗口起始时间戳（当前时间戳-60s）的数据。</span></li>
   <li fid="u07213dc0" data-lake-id="u9cc966b6" id="u9cc966b6"><span data-lake-id="u1cfdfdbe" id="u1cfdfdbe">最后，再使用ZCARD命令来获取有序集合中的成员数量，即在窗口内的请求量。</span></li>
  </ol>
  <p data-lake-id="ube972da0" id="ube972da0"><span data-lake-id="u7806e02a" id="u7806e02a">​</span><br></p>
  <pre lang="java"><code>
import redis.clients.jedis.Jedis;

public class SlidingWindowRateLimiter {
    private Jedis jedis;
    private String key;
    private int limit;

    public boolean allowRequest(String key) {
        //当前时间戳
        long currentTime = System.currentTimeMillis();
        //窗口开始时间是当前时间减60s
        long windowStart = currentTime - 60 * 1000;
        //删除窗口开始时间之前的所有数据
        jedis.zremrangeByScore(key, "-inf", String.valueOf(windowStart));
        //计算总请求数
        long currentRequests = jedis.zcard(key);
    	//窗口足够则把当前请求加入
        if (currentRequests &lt; limit) {
            jedis.zadd(key, currentTime, String.valueOf(currentTime));
            return true;
        }

        return false;
    }
}

</code></pre>
  <p data-lake-id="u1afc2733" id="u1afc2733"><span data-lake-id="u12ef33c8" id="u12ef33c8">​</span><br></p>
  <p data-lake-id="uce08eb5c" id="uce08eb5c"><span data-lake-id="uf36abfb5" id="uf36abfb5">以上代码在高并发情况下，可能会存在原子性的问题，需要考虑加事务或者lua脚本：</span></p>
  <p data-lake-id="u2cb3febd" id="u2cb3febd"><span data-lake-id="uf4e2449e" id="uf4e2449e">​</span><br></p>
  <pre lang="java"><code>
import redis.clients.jedis.Jedis;

public class SlidingWindowRateLimiter {
    private Jedis jedis;
    private String key;
    private int limit;

    public SlidingWindowRateLimiter(Jedis jedis, String key, int limit) {
        this.jedis = jedis;
        this.key = key;
        this.limit = limit;
    }

    public boolean allowRequest(String key) {
        // 当前时间戳
        long currentTime = System.currentTimeMillis();

        // 使用Lua脚本来确保原子性操作
        String luaScript = "local window_start = ARGV[1] - 60000\n" +
                           "redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', window_start)\n" +
                           "local current_requests = redis.call('ZCARD', KEYS[1])\n" +
                           "if current_requests &lt; tonumber(ARGV[2]) then\n" +
                           "    redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1])\n" +
                           "    return 1\n" +
                           "else\n" +
                           "    return 0\n" +
                           "end";

        Object result = jedis.eval(luaScript, 1, key, String.valueOf(currentTime), String.valueOf(limit));
        
        return (Long) result == 1;
    }
}

</code></pre>
  <h1 data-lake-id="K2V6w" id="K2V6w"><span data-lake-id="ue3f5a973" id="ue3f5a973">扩展知识</span></h1>
  <p data-lake-id="uca90d214" id="uca90d214"><br></p>
  <h1 data-lake-id="fa39M" id="fa39M"><span data-lake-id="ua2bc2bf5" id="ua2bc2bf5" style="color: rgb(0, 0, 0)">ZREMRANGEBYSCORE </span></h1>
  <p data-lake-id="ud0174985" id="ud0174985"><br></p>
  <p data-lake-id="uf35255cb" id="uf35255cb"><span data-lake-id="ub7c50b35" id="ub7c50b35" style="color: rgb(0, 0, 0)"> </span><span data-lake-id="u08c7ebfe" id="u08c7ebfe" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">Redis Zremrangebyscore 命令用于移除有序集中，指定分数（score）区间内的所有成员。</span></p>
  <p data-lake-id="u63fdfaae" id="u63fdfaae"><span data-lake-id="u23754906" id="u23754906">​</span><br></p>
  <p data-lake-id="udc5d9077" id="udc5d9077"><span data-lake-id="u8da0046c" id="u8da0046c" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">redis Zremrangebyscore 命令基本语法如下：</span></p>
  <p data-lake-id="u75c5f2df" id="u75c5f2df"><br></p>
  <pre lang="java"><code>
 ZREMRANGEBYSCORE key min max
</code></pre>
  <p data-lake-id="u981fd786" id="u981fd786"><br></p>
  <p data-lake-id="u44856591" id="u44856591"><span data-lake-id="u87f959d9" id="u87f959d9" class="lake-fontsize-11">而我们代码中使用的'-inf'在redis中表</span><span data-lake-id="u67d77607" id="u67d77607">示负无穷。-inf代表负无穷，+inf代表正无穷</span></p>
 </body>
</html>